package org.psyduck.security.utils;

import io.jsonwebtoken.*;
import org.psyduck.common.exception.BusinessException;
import org.psyduck.common.model.JsonCode;
import org.psyduck.common.utils.Global;
import org.psyduck.security.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class JwtUtils {

    private static final String CLAIM_KEY_AUTHORITIES = "scope";

    private List<String> tokens = new ArrayList<>();

    private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;

    public User getUserFromToken(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            if (null == claims) {
                return null;
            }
            String userId = getUserIdFromToken(token);
            String username = claims.getSubject();
            return User.builder().id(userId).username(username).build();
        } catch (Exception e) {
            throw new BusinessException(JsonCode.JWT_GET_USER_TOKEN_ERROR,e.getMessage());
        }
    }

    public String getUserIdFromToken(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            if (null == claims) {
                return null;
            }
            return String.valueOf(claims.get(Global.CLAIM_KEY_USER_ID));
        } catch (Exception e) {
            throw new BusinessException(JsonCode.JWT_GET_USER_ID_ERROR,e.getMessage());
        }
    }

    public String getUsernameFromToken(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            if (null == claims) {
                return null;
            }
            return claims.getSubject();
        } catch (Exception e) {
            throw new BusinessException(JsonCode.JWT_GET_USER_NAME_ERROR,e.getMessage());
        }
    }

    public String generateAccessToken(User user) {
        Map<String, Object> claims = generateClaims(user);
        if(null != user.getAuthorities() && user.getAuthorities().size() > 0){
            claims.put(CLAIM_KEY_AUTHORITIES, authoritiesToArray(user.getAuthorities()).get(0));
        }
        return generateAccessToken(user.getUsername(), claims);
    }

    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }


    public Boolean validateToken(String token, UserDetails userDetails) {
        User user = (User) userDetails;
        String userId = getUserIdFromToken(token);
        String username = getUsernameFromToken(token);
        return (userId.equals(user.getId())
            && username.equals(user.getUsername())
            && !isTokenExpired(token)
        );
    }

    public void putToken(String token) {
        tokens.add(token);
    }

    public void deleteToken(String userName) {
        tokens.remove(userName);
    }

    public boolean containToken(String token) {
        if (token != null && tokens.contains(token)) {
            return true;
        }
        return false;
    }
    private Claims getClaimsFromToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(Global.JWT_SECRET)
                    .parseClaimsJws(token)
                    .getBody();
        } catch(ExpiredJwtException e) {
            tokens.remove(token);
            return null;
        } catch (Exception e) {
            throw new BusinessException(JsonCode.JWT_GET_CLAIMS_ERROR,e.getMessage());
        }
    }

    private Date generateExpirationDate(long expiration) {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    private Map<String, Object> generateClaims(User user) {
        Map<String, Object> claims = new HashMap<>(16);
        claims.put(Global.CLAIM_KEY_USER_ID, user.getId());
        return claims;
    }

    private String generateAccessToken(String subject, Map<String, Object> claims) {
        return generateToken(subject, claims, Global.ACCESS_TOKEN_EXPIRATION);
    }

    private List authoritiesToArray(Collection<? extends GrantedAuthority> authorities) {
        List<String> list = new ArrayList<>();
        for (GrantedAuthority ga : authorities) {
            list.add(ga.getAuthority());
        }
        return list;
    }

    private String generateToken(String subject, Map<String, Object> claims, long expiration) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(new Date())
                .setExpiration(generateExpirationDate(expiration))
                .compressWith(CompressionCodecs.DEFLATE)
                .signWith(SIGNATURE_ALGORITHM, Global.JWT_SECRET)
                .compact();
    }

}
